Nom : Aka Yei Noellie , Lignier Léa
M2 MSID
A l'ère de l'intelligence artficielle,l'automatisation des tâches à faible valeur ajoutée est devenue une problématique cruciale pour les entreprises. Ces dernières trouvent une solution dans le Deep Learning, en passant par ses méthodes les plus récentes au plus anciennes.
Un exemple populaire d'utilisation pratique est dans le domaine de la classification d'images. Grâce aux données de la base MNIST, nous essayerons de vous montrer comment le modèle du Perceptron Multi-couches peut être utilisé pour reconnaître des chiffres manuscrits.
Dans cette partie, nous expliquerons et implémenterons toutes les fonctions utiles à la réalisation de notre classification.
#librairie traitement des données
import numpy as np
import pandas as pd
# Librairie graphiques
import matplotlib
import matplotlib.pyplot as plt
import plotly
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import plotly.express as px
import seaborn as sns
import plotly.io as pio
# Librairies Stat
import tensorflow as tf
from tensorflow import keras
import sklearn
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import confusion_matrix
import statistics
#Librairies Système
from datetime import datetime
import os
print("numpy V. ", np.__version__)
print("pandas V. ", pd.__version__)
print("tensorflow V. ", tf.__version__)
print("Matplotlib", matplotlib.__version__)
print("Plotly V. ", plotly.__version__)
print("sklearn V. ", sklearn.__version__)
print("seaborn V. ", sns.__version__)
numpy V. 1.23.2 pandas V. 1.4.3 tensorflow V. 2.10.0 Matplotlib 3.5.3 Plotly V. 5.10.0 sklearn V. 0.24.2 seaborn V. 0.12.1
Model_PMC est la fonction qui nous permettra de créer notre perceptron multi-couches. Dans cette fonction, on définit à la suite les couches cachées et la couche de sortie. Les hyper-paramètres sur lesquelles nous jouerons pour l'ensemble de ces couches sont : le nombre de neurones (units) et les fonctions d'activation (activation). Nous compilerons ensuite le modèle.
def Model_PMC (n_cc,p_cc,p_comp,p_sort) :
model = keras.Sequential()
# Définition des couches cachées
for i in range(n_cc) :
model.add(keras.layers.Dense(p_cc['n_neur'][i],activation=p_cc['act'][i]))
# Définition de la couche de sortie
model.add(keras.layers.Dense(p_sort['n_class'],activation=p_sort['act']))
# Compilation du modèle
model.compile(optimizer=p_comp['opt'],loss=p_comp['loss'],metrics=p_comp['metrics'])
return model
Nous allons créer une fonction capable de tracer l'accuracy et la fonction de perte pour les données d'apprentissage et de validation.
*Nb* : Si loss_val = None, les courbes des données de validation ne seront pas tracées.
def plot_qualit (loss_train,acc_train,loss_val=None,acc_val=None) :
if loss_val is not None :
fig = make_subplots(rows=2, cols=1, subplot_titles=("Loss", "Accuracy"))
fig.add_trace(
go.Scatter(y=loss_train,name='loss train'),
row=1, col=1,secondary_y=False
)
fig.add_trace(
go.Scatter (y=loss_val,name='loss val'),
row=1, col=1, secondary_y=False
)
fig.add_trace(
go.Scatter(y=acc_train,name='accuracy train'),
row=2, col=1, secondary_y=False,
)
fig.add_trace(
go.Scatter(y=acc_val,name='accuracy val'),
row=2, col=1, secondary_y=False,
)
fig.update_layout(height=800, width=800, title_text="Fonction de perte et accuracy")
pio.renderers.default='notebook'
fig.show("notebook")
else :
fig = make_subplots(rows=2, cols=1,subplot_titles=("Loss", "Accuracy"))
fig.add_trace(
go.Scatter(y=loss_train,name='loss train'),
row=1, col=1
)
fig.add_trace(
go.Scatter(y=acc_train,name='accuracy loss'),
row=2, col=1
)
fig.update_layout(height=800, width=800, title_text="Fonction de perte et accuracy")
pio.renderers.default='notebook'
fig.show("notebook")
La fonction plot_img nous permet de visualiser les images d'un dataset.
*Nb:* Si la valeur de prédiction est true, les images seront affichées en rouge et leurs mauvaises prédictions en dessous.
def plot_img (data,label,prediction=True) :
diviseurs = [i for i in range(1,len(data)+1) if len(data) % i == 0]
data = data.values.reshape(data.shape[0], 28, 28,1)
if len(diviseurs)>=2 :
n_cols = int(diviseurs[len(diviseurs) // 2])
else :
n_cols = len(data)
n_rows = int(len(data)/n_cols)
fig, ax = plt.subplots(nrows=n_rows, ncols=n_cols, figsize=(20, 4))
ax=ax.flatten()
if prediction is False :
for i in range(len(data)):
ax[i].imshow(data[i], cmap='gray')
ax[i].set_xlabel(label[i])
else :
for i in range(len(data)):
ax[i].imshow(data[i], cmap='Reds')
ax[i].set_xlabel(label[i])
plt.tight_layout()
plt.show()
Nous avons 60000 images dans le fichier "mnist_train-e.csv" et 10000 dans le fichier "mnist_test-e.csv". Observons la répartition des classes dans notre fichier train :
data = pd.read_csv("mnist_train-e.csv")
y = data.pop('1')
sns.histplot(data=y);
Nous observons que certaines classes ont plus d'effectifs que d'autres. Dans un soucis d'équilibre, nous allons découper les données du fichier "mnist_train-e.csv" de la manière suivante :
new_idx = data.index[y==0][:4800]
rest_idx = data.index[y==0][4800:]
for i in np.arange(1,10) :
new_idx = new_idx.append(data.index[y==i][:4800])
rest_idx = rest_idx.append(data.index[y==i][4800:])
train_data= data.iloc[new_idx]
y_train = y[new_idx]
val_data= data.iloc[rest_idx]
y_val = y[rest_idx]
test_data = pd.read_csv("mnist_test-e.csv")
y_test =test_data.pop('1')
print("Les cinq premières lignes des données d'apprentissage:")
train_data.head(5)
Les cinq premières lignes des données d'apprentissage:
| 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ... | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 21 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 34 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 37 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 51 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
5 rows × 784 columns
print("Visualisation des 12 premières images de test:")
plot_img(test_data[0:12],y_test[0:12],prediction=False)
Visualisation des 12 premières images de test:
Le deuxième traitement que nous allons appliquer à nos images est la normalisation des données grâce à l'étendue. Normaliser les données permet de ne pas faire exploser le calcul des fonctions et d'avoir une échelle restreinte entre 0 et 1.
$$ x_{new}=\frac{x_{old}-min}{max-min}$$
La fonction qui permet de faire cette opération avec Python est *MinMaxScaler* et provient du package sklearn.
Nos nouvelles données s'appelleront train_data_norm, val_data_norm et test_data_norm.
scaler = MinMaxScaler()
train_data_norm = pd.DataFrame(scaler.fit_transform(train_data))
val_data_norm = pd.DataFrame(scaler.fit_transform(val_data))
test_data_norm = pd.DataFrame(scaler.fit_transform(test_data))
Le seuillage d'image est une technique de binarisation d'image1, consistant à remplacer les niveaux de gris d'une image par un ensemble de pixels prenant la valeur 255 (blanc) ou 0 (noir) selon que sa valeur initiale est inférieure ou supérieure à une valeur définie comme seuil. Nous avons créé une fonction binarise qui prend en entrée :
Elle retourne le tableau de données avec de nouvelles coordonnées (1 ou 0)
def binarise(tab, seuil):
for i in range(0,tab.shape[0],1) :
for j in range(0,tab.shape[1],1) :
if tab[i][j] >= seuil :
tab[i][j] = 0 #noir
else :
tab[i][j] = 1 #blanc
return(tab)
*NB:* Le seuillage et la normalisation ont été appliqués directement sur le jeu de données équilibré et de façon indépendante.
Pour chaque traitement effectué dans les données précédentes, nous allons faire varier les paramètres, afficher l'accuracy et la fonction de perte. Nous allons entraîner nos données normalisées puis celles binarisées en faisant varier les hyper-paramètres.
Dans ce premier modèle, nous avons décidé d'utiliser deux couches et de faire une combinaison entre plusieurs valeurs de nombre de neurones allant de 50 à 550 pour décider du nombre de neurones à choisir dans chaque couche. En résultat, nous avons un tableau nommé *test1* qui donne les dernières valeurs de l'accuracy et de la fonction de perte pour les données d'apprentissage et de validation.
Le temps d'éxécution étant long, nous avons exporté nos résultats en fichier csv pour ne pas avoir à relancer le code ci-dessous.
n_cc = 2
cc1 = range(50,600,100) # couche cachée 1
cc2 = range(50,600,100) # couche cachée 2
test1= pd.DataFrame({'cc1' : [],'cc2':[],'loss_train':[],'loss_val':[],'acc_train':[],'acc_val':[]})
for i in cc1 :
for j in cc2 :
if i>=j :
p_cc = {'n_neur':[i,j],'act':['relu','relu']}
p_sort = {'n_class':10,'act':'softmax'}
p_comp = {'opt':'adam','loss':keras.losses.SparseCategoricalCrossentropy(),'metrics':'accuracy'}
model = Model_PMC (n_cc,p_cc,p_comp,p_sort)
model_entrain_norm = model.fit(train_data_norm,y_train,validation_data=(val_data_norm,y_val),epochs=10,batch_size=128,verbose=0)
loss_train_norm = model_entrain_norm.history['loss'][-1]
acc_train_norm = model_entrain_norm.history['accuracy'][-1]
loss_val_norm = model_entrain_norm.history['val_loss'][-1]
acc_val_norm = model_entrain_norm.history['val_accuracy'][-1]
new_row = pd.Series({'cc1':i,'cc2':j,'loss_train':loss_train_norm,'loss_val':loss_val_norm,'acc_train':acc_train_norm,'acc_val': acc_val_norm })
test1 = pd.concat([test1, new_row.to_frame().T], ignore_index=True)
print("Voici le resultat rangés en fonction de l'accuracy de test décroissant:")
test1.sort_values(by='acc_train',ascending=False)
Voici le resultat rangés en fonction de l'accuracy de test décroissant:
| cc1 | cc2 | loss_train | loss_val | acc_train | acc_val | |
|---|---|---|---|---|---|---|
| 13 | 450.0 | 350.0 | 0.007970 | 0.094575 | 0.997563 | 0.978917 |
| 10 | 450.0 | 50.0 | 0.010948 | 0.090550 | 0.996792 | 0.977083 |
| 11 | 450.0 | 150.0 | 0.010210 | 0.093104 | 0.996708 | 0.978583 |
| 3 | 250.0 | 50.0 | 0.012410 | 0.087733 | 0.996604 | 0.977917 |
| 6 | 350.0 | 50.0 | 0.012065 | 0.089200 | 0.996500 | 0.978917 |
| 16 | 550.0 | 150.0 | 0.012107 | 0.101927 | 0.996208 | 0.975333 |
| 18 | 550.0 | 350.0 | 0.011005 | 0.106295 | 0.996104 | 0.977750 |
| 15 | 550.0 | 50.0 | 0.012920 | 0.095132 | 0.996042 | 0.978333 |
| 12 | 450.0 | 250.0 | 0.012211 | 0.098431 | 0.995833 | 0.977667 |
| 14 | 450.0 | 450.0 | 0.013545 | 0.123933 | 0.995667 | 0.976417 |
| 7 | 350.0 | 150.0 | 0.012990 | 0.094927 | 0.995646 | 0.977333 |
| 9 | 350.0 | 350.0 | 0.013087 | 0.095526 | 0.995521 | 0.977417 |
| 8 | 350.0 | 250.0 | 0.013683 | 0.118810 | 0.995438 | 0.972417 |
| 17 | 550.0 | 250.0 | 0.013904 | 0.083680 | 0.995292 | 0.979000 |
| 20 | 550.0 | 550.0 | 0.013932 | 0.115236 | 0.995292 | 0.976167 |
| 2 | 150.0 | 150.0 | 0.015847 | 0.090988 | 0.995250 | 0.977000 |
| 4 | 250.0 | 150.0 | 0.015734 | 0.087259 | 0.995083 | 0.977833 |
| 19 | 550.0 | 450.0 | 0.015997 | 0.094665 | 0.994917 | 0.978500 |
| 5 | 250.0 | 250.0 | 0.016731 | 0.089275 | 0.994563 | 0.978583 |
| 1 | 150.0 | 50.0 | 0.020064 | 0.087363 | 0.994479 | 0.976917 |
| 0 | 50.0 | 50.0 | 0.061505 | 0.104928 | 0.981104 | 0.970250 |
test1.to_csv("result_test1.csv")
Nous allons retenir le résultat de la 13e ligne car nous obtenons pour les données d'apprentissage la plus petite valeur de la fonction perte 0.007970 et la meilleure accuracy 0.997563. Nous aurons ainsi *450* neurones sur la première couche cachée et *350* sur la deuxième.
Le nombre d'epochs est fixé à 10 et le batch à 128.
#hyperparamètres
n_cc = 2 # nombre de couches cachées
p_cc = {'n_neur':[450,350],'act':['relu','relu']} #nombre de neurones des couches cachées et fonction d'activation
p_sort = {'n_class':10,'act':'softmax'}
p_comp = {'opt':'adam','loss':keras.losses.SparseCategoricalCrossentropy(),'metrics':'accuracy'}
#Application :
model = Model_PMC (n_cc,p_cc,p_comp,p_sort)
model_entrain_norm = model.fit(train_data_norm,y_train,validation_data=(val_data_norm,y_val),epochs=10,batch_size=128,verbose=0)
loss_train_norm = model_entrain_norm.history['loss']
acc_train_norm = model_entrain_norm.history['accuracy']
loss_val_norm = model_entrain_norm.history['val_loss']
acc_val_norm = model_entrain_norm.history['val_accuracy']
# Fonction de perte et accuracy pour les données test
loss_test_norm, acc_test_norm= model.evaluate(test_data_norm,y_test)
print("Erreur d'appentissage:",round(loss_train_norm[-1],3))
print("Accuracy d'appentissage:",round(acc_train_norm[-1],3))
print("Erreur de test:",round(loss_test_norm,3))
print("Accuracy de test:",round(acc_test_norm,3))
# Representation
plot_qualit(loss_train_norm,acc_train_norm,loss_val=loss_val_norm,acc_val=acc_val_norm)
313/313 [==============================] - 2s 6ms/step - loss: 0.0715 - accuracy: 0.9827 Erreur d'appentissage: 0.01 Accuracy d'appentissage: 0.997 Erreur de test: 0.071 Accuracy de test: 0.983
#sauvegarde
model.save=("model_norm.h5")
Avec une faible valeur d'epochs, nous obtenons une valeur d'accuracy à 0.997. Augmentons le nombre d'epochs afin de voir si le modèle apprend mieux (toujours avec le couple (450,350))
model_norm1 = Model_PMC(n_cc,p_cc,p_comp,p_sort)
n_cc = 2 # nombre de couches cachées
p_cc = {'n_neur':[450,350],'act':['relu','relu']} # Nombre de neurones des couches cachées et fonction d'activation
p_sort = {'n_class':10,'act':'softmax'}
p_comp = {'opt':'adam','loss':keras.losses.SparseCategoricalCrossentropy(),'metrics':'accuracy'}
model_entrain_norm1 = model_norm1.fit(train_data_norm,y_train,validation_data=(val_data_norm,y_val),epochs=20,batch_size=128,verbose=0)
loss_train_norm1 = model_entrain_norm1.history['loss']
acc_train_norm1 = model_entrain_norm1.history['accuracy']
loss_val_norm1 = model_entrain_norm1.history['val_loss']
acc_val_norm1 = model_entrain_norm1.history['val_accuracy']
#Fonction de perte et accuracy pour les données test
loss_test_norm1, acc_test_norm1 = model_norm1.evaluate(test_data_norm,y_test)
print("Erreur d'appentissage:",round(loss_train_norm1[-1],3))
print("Accuracy d'appentissage:",round(acc_train_norm1[-1],3))
print("Erreur de test:",round(loss_test_norm1,3))
print("Accuracy de test:",round(acc_test_norm1,3))
plot_qualit(loss_train_norm1,acc_train_norm1,loss_val=loss_val_norm1,acc_val=acc_val_norm1)
313/313 [==============================] - 2s 5ms/step - loss: 0.1005 - accuracy: 0.9821 Erreur d'appentissage: 0.005 Accuracy d'appentissage: 0.998 Erreur de test: 0.1 Accuracy de test: 0.982
L'augmentation du nombre d'epochs permet d'améliorer faiblement notre modèle. En effet, l'accuracy des données d'apprentissage augmente *(+0.001)* et leur fonction de perte passe de 0.01 à 0.005. Quant aux données de test, elles ont dû mal à s'adapter à ce modèle. En effet, leur accuracy baisse de 0.001 avec une erreur d'apprentissage qui augmente de 0.004. Dans la suite, nous utiliserons le modèle avec 10 epochs.
model_norm1.save=("model_norm1.h5")
Nous utiliserons le modèle avec 10 epochs puisqu'il a une meilleure valeur d'accuracy de test.
# Prédictions test
predict_proba_norm = model.predict(test_data_norm);
predictions_norm= np.argmax(predict_proba_norm, axis=1);
print("Représentation de quelques mauvaises prédictions:")
plot_img(test_data_norm[y_test!=predictions_norm][0:5],label=predictions_norm[y_test!=predictions_norm][0:5],prediction=True)
313/313 [==============================] - 2s 5ms/step Représentation de quelques mauvaises prédictions:
mc_norm = pd.DataFrame(confusion_matrix(y_test,predictions_norm),columns=range(0,10))
mc_norm['Total mauvaises predict'] = [np.sum(mc_norm.iloc[i])-mc_norm.loc[i,i] for i in range(0,10)]
mc_norm
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Total mauvaises predict | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 971 | 1 | 1 | 0 | 0 | 1 | 2 | 0 | 2 | 2 | 9 |
| 1 | 0 | 1123 | 2 | 2 | 0 | 1 | 3 | 1 | 3 | 0 | 12 |
| 2 | 2 | 0 | 1016 | 2 | 3 | 0 | 1 | 5 | 3 | 0 | 16 |
| 3 | 0 | 0 | 3 | 991 | 0 | 9 | 0 | 2 | 4 | 1 | 19 |
| 4 | 1 | 0 | 2 | 0 | 956 | 1 | 5 | 6 | 0 | 11 | 26 |
| 5 | 2 | 0 | 0 | 4 | 0 | 879 | 4 | 0 | 2 | 1 | 13 |
| 6 | 4 | 2 | 0 | 1 | 2 | 2 | 947 | 0 | 0 | 0 | 11 |
| 7 | 1 | 0 | 6 | 2 | 0 | 0 | 0 | 1010 | 2 | 7 | 18 |
| 8 | 0 | 1 | 6 | 5 | 2 | 3 | 2 | 4 | 948 | 3 | 26 |
| 9 | 3 | 2 | 0 | 2 | 6 | 3 | 2 | 4 | 1 | 986 | 23 |
Les plus mauvaises prédictions viennent des images de la classe 4 et de la classe 8. Intéressons nous à la classe 4. 11 images de cette classe sont prédites comme appartenant à la classe 9. Affichons ces images mal prédites de cette classe. Pour rappel, les classes prédites par notre modèle sont affichées en dessous de chaque image.
plot_img(test_data_norm[(y_test==4) &(predictions_norm==9)][0:11],label=predictions_norm[(y_test==4) &(predictions_norm==9)][0:11],prediction=True)
Répresentons également quelques images de la classe 4 qui sont bien prédites :
plot_img(test_data_norm[(y_test==4) &(predictions_norm==4)][0:5],label=predictions_norm[(y_test==4) &(predictions_norm==4)][0:5],prediction=False)
Répresentons également quelques images de la classe 9 :
plot_img(test_data_norm[(y_test==9)][0:3],label=predictions_norm[(y_test==9)][0:3],prediction=False)
On peut observer que les images de la classe 4 qui sont prédites en 9 sont celles qui sont fermées ( voir ci-dessous à quoi correspond un 4 fermé) tandis que les 4 bien prédits sont ceux ouverts. Lorsque le "4 est ouvert", il est fort ressemblant au chiffre 9.
La classe ayant le meilleur taux de prédiction est la classe 0. Affichons les images mal prédites de cette classe :
plot_img(test_data_norm[(y_test==0) &(predictions_norm!=0)][0:9],label=predictions_norm[(y_test==0) &(predictions_norm!=0)][0:9],prediction=True)
On peut observer que ces images sont soient manquantes de quelques parties (exemple 2e image de la deuxième ligne) soient ont des traits en plus qui sont rajoutés (3e image de la troisième ligne). Ces erreurs sont principalement dûes à la mauvaise écriture manuscrite.
Nous allons représenter la distribution de nos valeurs de pixels dans nos données d'entraînement en supprimant les valeurs aberrantes (pixel <10 et pixel>199).
count_values = np.bincount(np.array(train_data).flatten())
plt.plot(range(10,199), count_values[10:199]);
plt.xlabel("Valeur de pixels")
plt.ylabel("Nombre de pixels")
plt.title("Distribution des pixels sur les données d'entraînement");
Nous allons choisir un seuil avant le premier pic (environ 68) pour essayer d'équilibrer la binarisation.
Nous allons ici réaliser la même configuration que le modèle n°2 mais en l'appliquant sur les données binarisées avec un seuil égal à la moyenne des valeurs de pixel, qui vient avant le premier pic.
Calculons la moyenne de nos données :
avg = round(np.mean(np.array(train_data).flatten()),0)
print("La moyenne est:",avg)
La moyenne est: 34.0
Binarisons les données avec ce nouveau seuil grâce à notre fonction binarise :
seuil = 34
train_data_bin = pd.DataFrame(binarise(np.array(train_data),seuil))
test_data_bin = pd.DataFrame(binarise(np.array(test_data),seuil))
val_data_bin = pd.DataFrame(binarise(np.array(val_data),seuil))
Entraînons ensuite le modèle avec les paramètres issus de la configuration du modèle 2 :
#hyper-paramètres
n_cc = 2 # Nombre de couches cachées
p_cc = {'n_neur':[450, 350], 'act':['relu','relu']} # Nombre de neurones des couches cachées et fonction d'activation
p_sort = {'n_class': 10,'act':'softmax'}
p_comp = {'opt':'adam','loss': keras.losses.SparseCategoricalCrossentropy(),'metrics':'accuracy'}
# entraînement
model_bin = Model_PMC(n_cc,p_cc,p_comp,p_sort)
model_entrain_bin = model_bin.fit(train_data_bin,y_train,validation_data=(val_data_bin,y_val),epochs=20,batch_size=128, verbose=0)
loss_train_bin = model_entrain_bin.history['loss']
acc_train_bin = model_entrain_bin.history['accuracy']
loss_val_bin = model_entrain_bin.history['val_loss']
acc_val_bin = model_entrain_bin.history['val_accuracy']
# Fonction de perte et accuracy pour les données test
loss_test_bin, acc_test_bin= model_bin.evaluate(test_data_bin,y_test)
print("Erreur d'appentissage:",round(loss_train_bin[-1],3))
print("Accuracy d'appentissage:",round(acc_train_bin[-1],3))
print("Erreur de test:",round(loss_test_bin,3))
print("Accuracy de test:",round(acc_test_bin,3))
# Representation
plot_qualit(loss_train_bin,acc_train_bin,loss_val=loss_val_bin,acc_val=acc_val_bin)
313/313 [==============================] - 2s 6ms/step - loss: 0.1030 - accuracy: 0.9726 Erreur d'appentissage: 0.034 Accuracy d'appentissage: 0.989 Erreur de test: 0.103 Accuracy de test: 0.973
Finalement, on constate que cette configuration est moins bien adaptée sur les données binarisées. En effet, on obtient une précision de 0.9726 sur l'ensemble test contre 0.9821 pour les données normalisées ce qui correspond à une baisse de 0,9 % en terme d'accuracy.
#sauvegarde
model_bin.save("model_bin.h5")
Au vu des résultats obtenus avec le modèle n°3 (où seuil = 34), nous allons expérimenter la même configuration mais en ajoutant une troisième couche cachée à 250 neurones, doubler le nombre d'epochs et le batch_size, modifier les fonctions d'activation des couches cachées pour tenter d'obtenir une meilleure accuracy :
# hyper-paramètres
n_cc = 3 # Nombre de couches cachées
p_cc = {'n_neur':[450,350,250],'act':['sigmoid','sigmoid','sigmoid']} # Nombre de neurones des couches cachées et fonction d'activation
p_sort = {'n_class':10,'act':'softmax'}
p_comp = {'opt':'adam','loss': keras.losses.SparseCategoricalCrossentropy(),'metrics':'accuracy'}
# Entraînement
model_bin1 = Model_PMC(n_cc,p_cc,p_comp,p_sort)
model_entrain_bin1 = model_bin1.fit(train_data_bin,y_train,validation_data=(val_data_bin,y_val),epochs=40,batch_size=256,verbose=0)
loss_train_bin1 = model_entrain_bin1.history['loss']
acc_train_bin1 = model_entrain_bin1.history['accuracy']
loss_val_bin1 = model_entrain_bin1.history['val_loss']
acc_val_bin1 = model_entrain_bin1.history['val_accuracy']
# Fonction de perte et accuracy pour les données test
loss_test_bin1, acc_test_bin1= model_bin1.evaluate(test_data_bin,y_test)
print("Erreur d'appentissage:",round(loss_train_bin1[-1],3))
print("Accuracy d'appentissage:",round(acc_train_bin1[-1],3))
print("Erreur de test:",round(loss_test_bin1,3))
print("Accuracy de test:",round(acc_test_bin1,3))
# Representation
plot_qualit(loss_train_bin1,acc_train_bin1,loss_val=loss_val_bin1,acc_val=acc_val_bin1)
313/313 [==============================] - 2s 6ms/step - loss: 0.0949 - accuracy: 0.9750 Erreur d'appentissage: 0.013 Accuracy d'appentissage: 0.996 Erreur de test: 0.095 Accuracy de test: 0.975
#Sauvegarde
model_bin1.save=("model_bin1.h5")
L'accuracy augmente de 0.02%. Nous allons tenter ainsi de modifier les nombres de neurones dans chacune des couches cachées toujours pour améliorer notre modèle.
On passe donc de (450,350,250) à (784,256,128) où 784 représente le nombre de variable de notre jeu de données et 256 le nombre de nuances de gris, ce qui correspond à rajouter 128 neurones à notre modèle avec une première couche plus importante.
n_cc = 3 # Nombre de couches cachées
p_cc = {'n_neur':[784,256,128],'act':['sigmoid','sigmoid','sigmoid']} #nombre de neurones des couches cachées et fonction d'activµation
p_sort = {'n_class':10,'act':'softmax'}
p_comp = {'opt':'adam','loss': keras.losses.SparseCategoricalCrossentropy(),'metrics':'accuracy'}
# Application
model_bin2 = Model_PMC (n_cc,p_cc,p_comp,p_sort)
model_entrain_bin2 = model_bin2 .fit(train_data_bin,y_train,validation_data=(val_data_bin,y_val),epochs=40,batch_size=256,verbose=0)
loss_train_bin2 = model_entrain_bin2.history['loss']
acc_train_bin2 = model_entrain_bin2.history['accuracy']
loss_val_bin2 = model_entrain_bin2.history['val_loss']
acc_val_bin2 = model_entrain_bin2.history['val_accuracy']
# Fonction de perte et accuracy pour les données test
loss_test_bin2, acc_test_bin2= model_bin2.evaluate(test_data_bin,y_test)
print("Erreur d'appentissage:",round(loss_train_bin2[-1],3))
print("Accuracy d'appentissage:",round(acc_train_bin2[-1],3))
print("Erreur de test:",round(loss_test_bin2,3))
print("Accuracy de test:",round(acc_test_bin2,3))
# Representation
plot_qualit(loss_train_bin2,acc_train_bin2,loss_val=loss_val_bin2,acc_val=acc_val_bin2)
313/313 [==============================] - 1s 3ms/step - loss: 0.0889 - accuracy: 0.9771 Erreur d'appentissage: 0.006 Accuracy d'appentissage: 0.999 Erreur de test: 0.089 Accuracy de test: 0.977
# Sauvegarde
model_bin2.save=("model_bin_2.h5")
L'accuracy sur l'ensemble test augmente à nouveau de 0.2% et l'accuracy des données test est à environ *0.999*. Augmentons ainsi le nombre d'epochs en le fixant à 50 au lieu de 40 afin d'observer si le modèle apprend mieux :
#hyper-paramètres
n_cc = 3 # Nombre de couches cachées
p_cc = {'n_neur':[784,256,128],'act':['sigmoid','sigmoid','sigmoid']} #nombre de neuronnes des couches cachées et fonction d'activµation
p_sort = {'n_class':10,'act':'softmax'}
p_comp = {'opt':'adam','loss': keras.losses.SparseCategoricalCrossentropy(),'metrics':'accuracy'}
# Application
model_bin3 = Model_PMC (n_cc,p_cc,p_comp,p_sort)
model_entrain_bin3 = model_bin3.fit(train_data_bin,y_train,validation_data=(val_data_bin,y_val),epochs=50,batch_size=256,verbose=0)
loss_train_bin3 = model_entrain_bin3.history['loss']
acc_train_bin3 = model_entrain_bin3.history['accuracy']
loss_val_bin3 = model_entrain_bin3.history['val_loss']
acc_val_bin3 = model_entrain_bin3.history['val_accuracy']
# Fonction de perte et accuracy pour les données test
loss_test_bin3, acc_test_bin3= model_bin3.evaluate(test_data_bin,y_test)
print("Erreur d'appentissage:",round(loss_train_bin3[-1],3))
print("Accuracy d'appentissage:",round(acc_train_bin3[-1],3))
print("Erreur de test:",round(loss_test_bin3,3))
print("Accuracy de test:",round(acc_test_bin3,3))
# Representation
plot_qualit(loss_train_bin3,acc_train_bin3,loss_val=loss_val_bin3,acc_val=acc_val_bin3)
313/313 [==============================] - 1s 3ms/step - loss: 0.0751 - accuracy: 0.9810 Erreur d'appentissage: 0.008 Accuracy d'appentissage: 0.997 Erreur de test: 0.075 Accuracy de test: 0.981
#Sauvegarde
model_bin3.save=("model_bin_3.h5")
Nous avons finalement réussi à obtenir une accuracy de test à 0.981 (+0.04) qui est assez proche du modèle 2 trouvé pour les données normalisées.
# Prédictions test
predict_proba_bin = model_bin3.predict(test_data_bin);
predictions_bin= np.argmax(predict_proba_bin, axis=1);
print("Représentation de quelques mauvaises prédictions:")
plot_img(test_data_bin[y_test!=predictions_bin][0:5],label=predictions_bin[y_test!=predictions_bin][0:5],prediction=True)
313/313 [==============================] - 1s 3ms/step Représentation de quelques mauvaises prédictions:
mc_bin = pd.DataFrame(confusion_matrix(y_test,predictions_bin),columns=range(0,10))
mc_bin['Total mauvaises predict'] = [np.sum(mc_bin.iloc[i])-mc_bin.loc[i,i] for i in range(0,10)]
mc_bin
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Total mauvaises predict | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 969 | 0 | 1 | 0 | 1 | 1 | 3 | 1 | 3 | 1 | 11 |
| 1 | 0 | 1123 | 3 | 2 | 0 | 0 | 3 | 0 | 4 | 0 | 12 |
| 2 | 5 | 0 | 1011 | 2 | 2 | 0 | 1 | 7 | 4 | 0 | 21 |
| 3 | 0 | 0 | 2 | 989 | 0 | 7 | 0 | 5 | 5 | 2 | 21 |
| 4 | 0 | 1 | 1 | 0 | 966 | 0 | 3 | 1 | 0 | 10 | 16 |
| 5 | 3 | 0 | 0 | 6 | 1 | 873 | 5 | 1 | 1 | 2 | 19 |
| 6 | 1 | 2 | 0 | 0 | 6 | 3 | 943 | 0 | 3 | 0 | 15 |
| 7 | 0 | 3 | 7 | 1 | 1 | 0 | 0 | 1005 | 3 | 8 | 23 |
| 8 | 5 | 1 | 2 | 2 | 2 | 1 | 3 | 3 | 951 | 4 | 23 |
| 9 | 2 | 4 | 0 | 4 | 11 | 1 | 0 | 6 | 1 | 980 | 29 |
Avec ce modèle, la classe 9 est très mal prédite. Plus de 11 images de sa classe sont prédites comme des 4. C'est le contraire du modèle 2 avec les données normalisées qui prédit les 4 en 9.
Afin d'essayer d'interpréter ce résultat, affichons quelques images binarisées mal prédites :
plot_img(test_data_bin[(y_test==9) &(predictions_bin==4)][0:11],label=predictions_bin[(y_test==9) &(predictions_bin==4)][0:11],prediction=True)
plot_img(test_data_bin[(y_test==4)][0:5],label=predictions_bin[(y_test==4)][0:5],prediction=False)
La mauvaise écriture manuscrite et le fait de binariser nos données nous fait perdre de la précision à notre modèle.
Dans cette partie, nous avons créé un tableau récapitulatif de nos modèles :
n_couches = [2,2,2,3,3,3]
cc1_neur = [450,450,450,450,784,784]
cc2_neur = [350,350,350,350,256,256]
cc3_neur = cc3_a = [np.nan,np.nan,np.nan,250,128,128]
cc12_a =['relu','relu','relu','sigmoid','sigmoid','sigmoid']
cc3_a = [np.nan,np.nan,np.nan,'sigmoid','sigmoid','sigmoid']
epochs= [10,20,20,40,40,50]
batchs= [128,128,128,256,256,256]
acc_test = [acc_test_norm,acc_test_norm1,acc_test_bin,acc_test_bin1,acc_test_bin2,acc_test_bin3]
recap = pd.DataFrame({'Neuronnes couche 1':cc1_neur,'Neuronnes couche 2':cc2_neur,
'Neuronnes couche 3':cc3_neur,'Activation couche 1':cc12_a,
'Activation couche 2':cc12_a,'Activation couche 3':cc3_a,
'Epochs':epochs,'Batch':batchs,
'Accuracy test':acc_test,
'Traitement données': ['N','N','B','B','B','B']},
index=['Modèle 1','Modèle 2','Modèle 3','Modèle 4','Modèle 5','Modèle 6'])
print("Tableau recapitulatif")
recap
Tableau recapitulatif
| Neuronnes couche 1 | Neuronnes couche 2 | Neuronnes couche 3 | Activation couche 1 | Activation couche 2 | Activation couche 3 | Epochs | Batch | Accuracy test | Traitement données | |
|---|---|---|---|---|---|---|---|---|---|---|
| Modèle 1 | 450 | 350 | NaN | relu | relu | NaN | 10 | 128 | 0.9827 | N |
| Modèle 2 | 450 | 350 | NaN | relu | relu | NaN | 20 | 128 | 0.9821 | N |
| Modèle 3 | 450 | 350 | NaN | relu | relu | NaN | 20 | 128 | 0.9726 | B |
| Modèle 4 | 450 | 350 | 250.0 | sigmoid | sigmoid | sigmoid | 40 | 256 | 0.9750 | B |
| Modèle 5 | 784 | 256 | 128.0 | sigmoid | sigmoid | sigmoid | 40 | 256 | 0.9771 | B |
| Modèle 6 | 784 | 256 | 128.0 | sigmoid | sigmoid | sigmoid | 50 | 256 | 0.9810 | B |
Meilleur modèle en terme d'accuracy de test : *Modèle 1*.
Il existe une infinité de configurations permettant de classifier les images de la base MNIST et il n'existe pas de solution "miracle". Afin de trouver le meilleur modèle MLP, nous sommes obligés de passer par une expérimentation en testant diverses combinaisons de paramètres car on ne sait pas d’avance quel est le choix optimal.
Ainsi, il est inévitable de passer par le nettoyage et la transformation des données pour éviter des problèmes d'ajustement lors de l'entraînement d'un tel modèle. Les résultats obtenus pour le modèle n°1 et le modèle n°6 confirment le fait que l'adaptation des paramètres dépend du jeu de données dont nous disposons ou du traitement que nous effectuons. En effet, notre première technique de traitement revient à normaliser les données pour les rendre homogènes alors que notre deuxième technique consiste à binariser les données, on se retrouve donc avec des valeurs égales à 0 ou à 1 et donc uniquement à des pixels noirs ou blancs. En enlevant les nuances de gris, on est suceptible de perdre de l'information notamment si nous sommes face à un jeu de données beaucoup plus complexe que celui des images MNIST ou si on choisit un seuil de binarisation trop élevé.
Par ailleurs, il faut réussir à faire un compromis entre une bonne précision et une faible perte possible tout en surveillant le problème d'overfitting ou d'underfitting grâce aux graphiques (on observe une bonne convergence de l'accuracy et de la fonction de perte pour chacun des modèles expérimentés). Comme nous l'avons vu tout au long de ce travail, la complexité d'un modèle MLP ainsi que sa performance se définissent principalement par l'initialisation des paramètres tels que le nombre de couches cachées, le nombre de neurones, le nombre d'epochs ainsi que le nombre de batchs.
Dans notre cas, c'est finalement le modèle n°1 que nous allons choisir pour prédire les données MNIST, en obtenant une accuracy test de 0.9827 (très bonne capacité de généralisation du modèle) et d'apprentissage à 0,997.